package com.ar4j;

import java.lang.reflect.Method;

import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.interceptor.TransactionAttribute;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;

import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import com.ar4j.util.ClassUtils;

/**
 * Main delegating invocation handler for all active record proxies. Simply uses the map in the given
 * active record meta data to invoke a specific IMethodHandler for the current active record class.
 */
public class DelegatingMethodInterceptor implements MethodInterceptor {
  private ActiveRecordMetadata metadata;
  private ActiveRecord<?> defaultImplementation;
  
  public DelegatingMethodInterceptor(ActiveRecordMetadata metadata) {
    this.metadata = metadata;
  }

  @Override
  public Object intercept(final Object obj, final Method method, final Object[] args, final MethodProxy proxy) throws Throwable {
    if(defaultImplementation == null) {
      throw new IllegalStateException("Cannot operate without an instance of the default implementation");
    }
    
    final ActiveRecord<?> dataHolder = (ActiveRecord<?>) obj;
    TransactionAttribute transactionAttribute = metadata.getMethodTransactionAttributeMap().get(method);
    Object out = null;
    if(transactionAttribute != null && transactionAttribute.getPropagationBehavior() != TransactionDefinition.PROPAGATION_NOT_SUPPORTED) {
      if(defaultImplementation.getContext().getPlatformTransactionManager() == null) {
        throw new IllegalStateException("Cannot execute transactional active record method without a platform transaction manager in the context, method: " + method);
      }
      
      TransactionTemplate template = new TransactionTemplate(defaultImplementation.getContext().getPlatformTransactionManager(), transactionAttribute);
      out = template.execute(new TransactionCallback<Object>() {
        @Override
        public Object doInTransaction(TransactionStatus status) {
          try { return handleCall(method, obj, args, dataHolder, proxy); }
          catch(Throwable e) { throw new RuntimeException("An error occured executing handler within a transaction", e); }
        }
      });
    } else {
      out = handleCall(method, obj, args, dataHolder, proxy);
    }
    
    return out;
  }

  @Override
  public String toString() {
    return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
  }
  
  public ActiveRecord<?> getDefaultImplementation() {
    return defaultImplementation;
  }

  public void setDefaultImplementation(ActiveRecord<?> defaultImplementation) {
    this.defaultImplementation = defaultImplementation;
  }

  /**
   * Handle a call to an active record method, multiplexing by the method type to figure out if we should
   * let the invocation pass through to the instance, execute it on the default implementation, or execute
   * via dynamic method handler.
   */
  private Object handleCall(Method method, Object obj, Object[] args, ActiveRecord<?> dataHolder, MethodProxy proxy) 
    throws Throwable {
    
    Object out = null;
    switch(metadata.getMethodTypeMap().get(method)) {
      case GET_PROPERTY:
        out = handleGetProperty(obj, args);
        break;
      
      case SET_PROPERTY:
        out = handleSetProperty(obj, args);
        break;
        
      case PASSTHROUGH:
        out = proxy.invokeSuper(obj, args);
        break;
        
      case HANDLED_BY_DEFAULT_IMPLEMENTATION:
        Method defaultMethod = metadata.getDefaultImplementationMethodMap().get(method);
        out = defaultMethod.invoke(defaultImplementation, args);
        break;
        
      case DYNAMIC:
        DynamicMethodMetadata dynamicMetadata = metadata.getDynamicMethodMetadataMap().get(method);
        out = DynamicMethodHandler.handleDynamicMethod(metadata, defaultImplementation.getContext(), dynamicMetadata, dataHolder, args);
        break;
        
      default:
        throw new RuntimeException("Unknown type of method: " + method);
    }
    
    return out;
  }
  
  /**
   * Handler that is able to retrieve an active record's property value via it's read method.
   */
  private Object handleGetProperty(Object target, Object[] args) 
    throws Throwable {
    
    // first arg must be the property name
    String name = (String) args[0];
    
    // find the read method, error out if there is none
    Method readMethod = metadata.getPropertyReadMethods().get(name);
    if(readMethod == null) {
      throw new IllegalArgumentException("Cannot get property value for " + name + " in " + metadata.getBaseClass() + " because the read method is missing");
    }
    
    // invoke and get the value
    Object value = readMethod.invoke(target);
    
    // coerce to a type if we were given one
    if(args.length > 1) {
      Class<?> requiredType = (Class<?>) args[1];
      value = ClassUtils.coerceValueToType(value, requiredType);
    }
    
    return value;
  }
  
  /**
   * Handler that is able to set an active record's property value via it's write method.
   */
  private Object handleSetProperty(Object target, Object[] args) 
    throws Throwable {
    
    // first arg must be the property name
    String name = (String) args[0];
    
    // find the write method, error out if there is none
    Method writeMethod = metadata.getPropertyWriteMethods().get(name);
    if(writeMethod == null) {
      throw new IllegalArgumentException("Cannot set property value for " + name + " in " + metadata.getBaseClass() + " because the write method is missing");
    }
    
    // invoke and get the value
    writeMethod.invoke(target, args[1]);
    
    // always return the target itself
    return target;
  }
}
